package com.example.ploy4j.service.impl;

import com.example.ploy4j.entity.DeploymentJob;
import com.example.ploy4j.entity.DeploymentStatus;
import com.example.ploy4j.entity.GitRepositoryConfig;
import com.example.ploy4j.entity.ServerConfig;
import com.example.ploy4j.repository.DeploymentJobRepository;
import com.example.ploy4j.repository.GitRepositoryConfigRepository;
import com.example.ploy4j.repository.ServerConfigRepository;
import com.example.ploy4j.service.DeploymentService;
import com.jcraft.jsch.*;
import jakarta.persistence.EntityNotFoundException;
import org.hibernate.Hibernate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;

import java.io.*;
import java.time.LocalDateTime;
import java.util.regex.Pattern;

// Custom exception for Maven build failures
class MavenBuildException extends RuntimeException {
    private final int exitCode;
    public MavenBuildException(String message, int exitCode) {
        super(message);
        this.exitCode = exitCode;
    }
    public int getExitCode() {
        return exitCode;
    }
}

@Service
public class DeploymentServiceImpl implements DeploymentService {

    private static final Logger logger = LoggerFactory.getLogger(DeploymentServiceImpl.class);

    private final GitRepositoryConfigRepository gitRepositoryConfigRepository;
    private final ServerConfigRepository serverConfigRepository;
    private final DeploymentJobRepository deploymentJobRepository;
    private final TransactionTemplate transactionTemplate;

    private static final String DEFAULT_MAVEN_BUILD_COMMAND_ARGS = "clean install -DskipTests"; // Just args
    private static final String MAVEN_ABSOLUTE_PATH = "/usr/local/apache-maven-3.8.1/bin/mvn"; // User-provided absolute path

    public DeploymentServiceImpl(GitRepositoryConfigRepository gitRepositoryConfigRepository,
                                 ServerConfigRepository serverConfigRepository,
                                 DeploymentJobRepository deploymentJobRepository,
                                 PlatformTransactionManager transactionManager) {
        this.gitRepositoryConfigRepository = gitRepositoryConfigRepository;
        this.serverConfigRepository = serverConfigRepository;
        this.deploymentJobRepository = deploymentJobRepository;
        this.transactionTemplate = new TransactionTemplate(transactionManager);
        this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
    }

    @Override
    @Async("asyncTaskExecutor")
    @Transactional
    public void startDeploymentPipeline(Long deploymentJobId) {
        StringBuilder currentLogSegment = new StringBuilder();
        DeploymentJob job = null; 
        logger.info("开始异步执行部署任务，任务ID: {}", deploymentJobId);
        try {
            job = deploymentJobRepository.findById(deploymentJobId)
                    .orElseThrow(() -> new EntityNotFoundException("DeploymentJob not found with id: " + deploymentJobId));
            logger.info("找到部署任务，任务ID: {}", deploymentJobId);
            Hibernate.initialize(job.getGitRepositoryConfig());
            Hibernate.initialize(job.getServerConfig());

        } catch (EntityNotFoundException e) {
            logger.warn("Cannot start deployment pipeline. DeploymentJob not found with id: {}. Error: {}", deploymentJobId, e.getMessage());
            return;
        }

        GitRepositoryConfig repoConfig = job.getGitRepositoryConfig();
        ServerConfig serverConfig = job.getServerConfig();
        String targetBranch = job.getBranchToDeploy();
        String projectName = repoConfig.getProjectName();
        String sanitizedProjectName = projectName.replaceAll("[^a-zA-Z0-9.-]", "_");
        String remoteBaseDir = String.format("~/.ploy4j_workspaces/%s_job%d", sanitizedProjectName, deploymentJobId);
        String remoteProjectDir = remoteBaseDir + "/" + sanitizedProjectName; 

        try {
            updateJobStatusAndLog(deploymentJobId, DeploymentStatus.PENDING, // Start with PENDING
                String.format("Deployment job %d accepted. Preparing remote workspace on server %s...%n", deploymentJobId, serverConfig.getHost()),
                false, LocalDateTime.now(), null);
            currentLogSegment.append(String.format("Starting deployment pipeline for job ID: %d at %s...%n", deploymentJobId, LocalDateTime.now()));
            currentLogSegment.append(String.format("Project: %s, Server: %s (%s), Branch: %s%n",
                    projectName, serverConfig.getName(), serverConfig.getHost(), targetBranch));
            currentLogSegment.append(String.format("Remote working directory will be: %s%n", remoteBaseDir));

            String prepareWorkspaceCommand = String.format("mkdir -p %1$s && rm -rf %2$s/* && mkdir -p %2$s", remoteBaseDir, remoteProjectDir);
            currentLogSegment.append(String.format("Preparing remote workspace with command: [%s]%n", prepareWorkspaceCommand));
            executeRemoteCommand(serverConfig, prepareWorkspaceCommand, currentLogSegment, "PREPARE_WORKSPACE");
            updateJobStatusAndLog(deploymentJobId, null, currentLogSegment.toString(), true, null, null);
            currentLogSegment.setLength(0);

            updateJobStatusAndLog(deploymentJobId, DeploymentStatus.GIT_PULLING, String.format("Starting remote Git operations in %s...%n", remoteProjectDir), true, null, null);
            String gitToken = repoConfig.getGiteeToken();
            String repoUrl = repoConfig.getRepositoryUrl();
            if (gitToken != null && !gitToken.isBlank() && repoUrl.startsWith("https")) {
                repoUrl = repoUrl.replace("https://", "https://oauth2:" + gitToken + "@");
                currentLogSegment.append(String.format("Using token for Git HTTPS clone (token embedded in URL - review security).%n"));
            }
            String gitCommand = String.format("git clone --branch %s --depth 1 %s %s", targetBranch, repoUrl, remoteProjectDir);
            
            currentLogSegment.append(String.format("Executing remote Git command: [%s]%n", gitCommand));
            executeRemoteCommand(serverConfig, gitCommand, currentLogSegment, "GIT_CLONE");
            updateJobStatusAndLog(deploymentJobId, DeploymentStatus.GIT_PULLED, currentLogSegment.toString(), true, null, null);
            currentLogSegment.setLength(0);

            updateJobStatusAndLog(deploymentJobId, DeploymentStatus.BUILDING, String.format("Starting remote Maven build in %s...%n", remoteProjectDir), true, null, null);
            
            boolean mavenBuildSuccess = false;
            try {
                String mavenBuildCommandPrimary = String.format("cd %s && if [ -f mvnw ]; then ./mvnw %s; else mvn %s; fi",
                                                    remoteProjectDir,
                                                    DEFAULT_MAVEN_BUILD_COMMAND_ARGS,
                                                    DEFAULT_MAVEN_BUILD_COMMAND_ARGS);
                currentLogSegment.append(String.format("Attempting Maven build (primary): [%s]%n", mavenBuildCommandPrimary));
                executeRemoteCommand(serverConfig, mavenBuildCommandPrimary, currentLogSegment, "MAVEN_BUILD_PRIMARY");
                mavenBuildSuccess = true;
            } catch (MavenBuildException e) {
                currentLogSegment.append(String.format("[WARN] Primary Maven build attempt failed: %s%n", e.getMessage()));
                logger.warn("Primary Maven build attempt failed with exit code {}. Message: {}. Trying fallback with absolute path.", e.getExitCode(), e.getMessage());
                
                // Check if it was a 'command not found' type error specifically for 'mvn'
                if (e.getExitCode() == 127 || (e.getMessage() != null && e.getMessage().toLowerCase().contains("mvn: command not found"))) {
                    updateJobStatusAndLog(deploymentJobId, null, currentLogSegment.toString(), true, null, null); // Log the primary failure
                    currentLogSegment.setLength(0); // Clear for fallback log
                    currentLogSegment.append(String.format("[INFO] Retrying Maven build with absolute path: %s%n", MAVEN_ABSOLUTE_PATH));
                    
                    String mavenBuildCommandFallback = String.format("cd %s && if [ -f mvnw ]; then ./mvnw %s; else %s %s; fi",
                                                                remoteProjectDir,
                                                                DEFAULT_MAVEN_BUILD_COMMAND_ARGS,
                                                                MAVEN_ABSOLUTE_PATH,
                                                                DEFAULT_MAVEN_BUILD_COMMAND_ARGS);
                    currentLogSegment.append(String.format("Attempting Maven build (fallback): [%s]%n", mavenBuildCommandFallback));
                    executeRemoteCommand(serverConfig, mavenBuildCommandFallback, currentLogSegment, "MAVEN_BUILD_FALLBACK");
                    mavenBuildSuccess = true; // If fallback succeeds
                } else {
                    throw e; // Re-throw if it was not a 'command not found' error, but a different Maven build issue
                }
            }

            if (!mavenBuildSuccess) {
                 // This case should ideally not be reached if exceptions are handled correctly above.
                throw new MavenBuildException("All Maven build attempts failed.", -1);
            }

            String remoteJarName = inferRemoteJarName(currentLogSegment.toString(), projectName, currentLogSegment);
            String remoteJarPathInProject = remoteProjectDir + "/target/" + remoteJarName;
            
            currentLogSegment.append(String.format("Inferred remote JAR: %s at %s%n", remoteJarName, remoteJarPathInProject));
            updateJobStatusAndLog(deploymentJobId, DeploymentStatus.BUILD_SUCCESS, currentLogSegment.toString(), true, null, null);
            currentLogSegment.setLength(0);

            final String finalRemoteJarName = remoteJarName;
            final String finalRemoteJarPath = remoteJarPathInProject;

            transactionTemplate.execute(status -> {
                DeploymentJob jobToUpdate = deploymentJobRepository.findById(deploymentJobId).orElseThrow();
                jobToUpdate.setJarFileName(finalRemoteJarName);
                jobToUpdate.setRemoteJarPath(finalRemoteJarPath);
                deploymentJobRepository.save(jobToUpdate);
                return null;
            });

            updateJobStatusAndLog(deploymentJobId, DeploymentStatus.KILLING_REMOTE, String.format("Attempting to stop existing remote process for JAR: %s...%n", finalRemoteJarName), true, null, null);
            killRemoteProcess(serverConfig, finalRemoteJarName, currentLogSegment); 
            updateJobStatusAndLog(deploymentJobId, DeploymentStatus.KILL_REMOTE_SUCCESS, currentLogSegment.toString(), true, null, null);
            currentLogSegment.setLength(0);

            updateJobStatusAndLog(deploymentJobId, DeploymentStatus.STARTING_REMOTE, String.format("Attempting to start new remote process for JAR: %s...%n", finalRemoteJarPath), true, null, null);
            startRemoteProcess(serverConfig, finalRemoteJarPath, currentLogSegment, sanitizedProjectName);
            updateJobStatusAndLog(deploymentJobId, DeploymentStatus.REMOTE_STARTED, currentLogSegment.toString(), true, null, null);
            currentLogSegment.setLength(0);

            currentLogSegment.append(String.format("Attempting to fetch initial application logs...%n"));
            fetchRemoteApplicationLog(serverConfig, sanitizedProjectName, currentLogSegment);
            updateJobStatusAndLog(deploymentJobId, null, currentLogSegment.toString(), true, null, null);
            currentLogSegment.setLength(0);

            updateJobStatusAndLog(deploymentJobId, DeploymentStatus.SUCCESS, "Deployment completed successfully!%n", true, null, LocalDateTime.now());
            logger.info("Deployment successful for job ID: {}", deploymentJobId);

        } catch (MavenBuildException e) { 
            logger.error("Maven build failed for job ID {}: ", deploymentJobId, e);
            currentLogSegment.append(String.format("%n%n****** MAVEN BUILD FAILED ******%nReason: %s%nExit Code: %d%n", e.getMessage(), e.getExitCode()));
            updateJobStatusAndLog(deploymentJobId, DeploymentStatus.BUILD_FAILED, currentLogSegment.toString(), true, null, LocalDateTime.now());
        } catch (Exception e) {
            logger.error("Deployment failed for job ID {}: ", deploymentJobId, e);
            currentLogSegment.append(String.format("%n%n****** DEPLOYMENT FAILED ******%nReason: %s%n", e.getMessage()));
            DeploymentStatus finalStatus = DeploymentStatus.FAILED;
            if (job != null) { // job might be null if initial findById failed, though we return early there.
                 DeploymentStatus currentStatus = deploymentJobRepository.findById(deploymentJobId).map(DeploymentJob::getStatus).orElse(DeploymentStatus.FAILED);
                 if (currentStatus == DeploymentStatus.KILLING_REMOTE) {
                    finalStatus = DeploymentStatus.KILL_REMOTE_FAILED;
                } else if (currentStatus == DeploymentStatus.STARTING_REMOTE || currentStatus == DeploymentStatus.REMOTE_STARTED) {
                    finalStatus = DeploymentStatus.REMOTE_START_FAILED;
                } else if (currentStatus == DeploymentStatus.UPLOADING) { // Though not used in this flow, good for completeness
                    finalStatus = DeploymentStatus.UPLOAD_FAILED;
                }
            }
            updateJobStatusAndLog(deploymentJobId, finalStatus, currentLogSegment.toString(), true, null, LocalDateTime.now());
        } finally {
            currentLogSegment.setLength(0);
            currentLogSegment.append(String.format("Remote workspace %s on server %s can be inspected for logs/artifacts.%n", remoteBaseDir, serverConfig.getHost()));
            currentLogSegment.append(String.format("Application logs (if started) should be at /root/%s_app.log on the remote server.%n", sanitizedProjectName));
            currentLogSegment.append("Consider adding a cleanup step for the remote workspace if not needed.");
            updateJobStatusAndLog(deploymentJobId, null, currentLogSegment.toString(), true, null, null);
            logger.info("Finished deployment pipeline for job ID: {}", deploymentJobId);
        }
    }
    
    private void updateJobStatusAndLog(Long jobId, DeploymentStatus newStatus, String logChunk, boolean appendLog, 
                                       LocalDateTime startedAt, LocalDateTime completedAt) {
        transactionTemplate.execute(status -> {
            DeploymentJob job = deploymentJobRepository.findById(jobId)
                    .orElseThrow(() -> new EntityNotFoundException("DeploymentJob not found with id: " + jobId + " while trying to update status/log."));

            if (newStatus != null) {
                job.setStatus(newStatus);
            }
            if (logChunk != null && !logChunk.isEmpty()) {
                if (appendLog) {
                    String currentLogs = job.getLogOutput() == null ? "" : job.getLogOutput();
                    job.setLogOutput(currentLogs + logChunk);
                } else {
                    job.setLogOutput(logChunk);
                }
            }
            if (startedAt != null && job.getStartedAt() == null) { 
                job.setStartedAt(startedAt);
            }
            if (completedAt != null) {
                job.setCompletedAt(completedAt);
            }
            deploymentJobRepository.save(job);
            return null;
        });
    }

    private void executeRemoteCommand(ServerConfig serverConfig, String command, StringBuilder log, String commandType) throws JSchException, IOException, MavenBuildException {
        log.append(String.format("Executing remote command (%s): [%s] on %s%n", commandType, command, serverConfig.getHost()));
        logger.info("Executing remote command ({}): [{}] on {}", commandType, command, serverConfig.getHost());
        Session session = null;
        ChannelExec channelExec = null;
        int exitStatus = -1; // Initialize with a non-zero value
        StringBuilder stdoutCollector = new StringBuilder();
        StringBuilder stderrCollector = new StringBuilder();

        try {
            session = createJschSession(serverConfig, log);
            channelExec = (ChannelExec) session.openChannel("exec");
            channelExec.setCommand(command);
            
            InputStream in = channelExec.getInputStream();
            InputStream err = channelExec.getErrStream();

            channelExec.connect();
            
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    log.append(String.format("[REMOTE STDOUT]: %s%n", line));
                    stdoutCollector.append(line).append("\n");
                }
            }
            try (BufferedReader errReader = new BufferedReader(new InputStreamReader(err))) {
                String line;
                while ((line = errReader.readLine()) != null) {
                    log.append(String.format("[REMOTE STDERR]: %s%n", line));
                    stderrCollector.append(line).append("\n");
                }
            }

            while(!channelExec.isClosed()){
                try {Thread.sleep(100);} catch(InterruptedException ie){ Thread.currentThread().interrupt(); }
            }
            exitStatus = channelExec.getExitStatus();

            log.append(String.format("Remote command (%s) finished with exit status: %d%n", commandType, exitStatus));
            logger.info("Remote command ({}) finished with exit status: {}", commandType, exitStatus);

            if (exitStatus != 0) {
                String errorDetails = String.format("Stderr: %s%nStdout: %s", stderrCollector.toString().trim(), stdoutCollector.toString().trim());
                String errorMessage = String.format("Remote command (%s) failed with exit status %d. Command: [%s].%n%s",
                                                    commandType, exitStatus, command, errorDetails);
                logger.warn(errorMessage);
                log.append(String.format("[ERROR] %s%n",errorMessage));

                if (commandType.startsWith("MAVEN_BUILD")) { // Covers MAVEN_BUILD_PRIMARY and MAVEN_BUILD_FALLBACK
                    throw new MavenBuildException(errorMessage, exitStatus);
                }
                
                boolean isNonCriticalFailure = command.startsWith("kill") || "APP_LOG_FETCH".equals(commandType);
                if (!command.startsWith("nohup") && !isNonCriticalFailure) { 
                     throw new IOException(errorMessage);
                }
            }
        } finally {
            if (channelExec != null) {
                channelExec.disconnect();
            }
            if (session != null) {
                session.disconnect();
            }
        }
    }

    private void killRemoteProcess(ServerConfig serverConfig, String jarFileName, StringBuilder log) throws JSchException, IOException {
        String pattern = String.format("java -jar.*%s", jarFileName.replace("$", "\\$"));
        String findAndKillCommand = String.format(
            "PIDS=$(pgrep -f '%s'); if [ -n \"$PIDS\" ]; then echo \"Found PIDs: $PIDS, attempting to kill...\"; kill -9 $PIDS || echo \"Kill command for PIDs $PIDS finished (may include errors if some PIDs already gone).\"; else echo \"No process found matching '%s'.\"; fi",
            pattern, pattern
        );
        log.append(String.format("Attempting to find and kill remote process matching pattern: '%s'%n", pattern));
        log.append(String.format("Executing actual kill command: [%s]%n", findAndKillCommand));
        logger.info("Attempting to find and kill remote process matching pattern: '{}' using command: [{}]", pattern, findAndKillCommand);
        try {
            executeRemoteCommand(serverConfig, findAndKillCommand, log, "KILL_PROCESS");
        } catch (IOException e) {
            log.append(String.format("Warning during kill process (command may have partially succeeded or failed gracefully): %s%n", e.getMessage()));
            logger.warn("Warning during kill process: {}", e.getMessage());
        } catch (MavenBuildException e) {
            // This should not happen for KILL_PROCESS, but defensive catch
             log.append(String.format("Unexpected MavenBuildException during kill process: %s%n", e.getMessage()));
            logger.warn("Unexpected MavenBuildException during kill process: {}", e.getMessage());
        }
        log.append(String.format("Kill command execution attempted. Check logs for details.%n"));
        logger.info("Kill command execution attempted.");
    }

    private void startRemoteProcess(ServerConfig serverConfig, String remoteJarFullPath, StringBuilder log, String sanitizedProjectName) throws JSchException, IOException {
        String remoteLogFileName = sanitizedProjectName + "_app.log";
        String logFilePath = "/root/" + remoteLogFileName; 

        String command = String.format("nohup java -jar %s > %s 2>&1 &", remoteJarFullPath, logFilePath);
        log.append(String.format("Attempting to start remote process. App log will be at: %s%nCommand: %s%n", logFilePath, command));
        logger.info("Attempting to start remote process. App log: {}. Command: {}", logFilePath, command);
        
        Session session = null;
        ChannelExec channelExec = null;
        try {
            session = createJschSession(serverConfig, log);
            channelExec = (ChannelExec) session.openChannel("exec");
            channelExec.setCommand(command);
            channelExec.connect(); 
            try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }

            log.append(String.format("Nohup command sent to start the application. Check server logs for application startup status at %s%n", logFilePath));
            logger.info("Nohup command sent. App log: {}", logFilePath);
        } finally {
            if (channelExec != null) { channelExec.disconnect(); }
            if (session != null) { session.disconnect(); }
        }
    }

    private void fetchRemoteApplicationLog(ServerConfig serverConfig, String sanitizedProjectName, StringBuilder logOutputAccumulator) {
        String remoteLogFileName = sanitizedProjectName + "_app.log";
        String logFilePath = "/root/" + remoteLogFileName; 
        
        String command = String.format("tail -n 50 %s", logFilePath);
        logOutputAccumulator.append(String.format("Executing command to fetch app log: [%s]%n", command));
        try {
            executeRemoteCommand(serverConfig, command, logOutputAccumulator, "APP_LOG_FETCH");
        } catch (Exception e) {
            String errorMsg = String.format("Warning: Failed to fetch remote application log from %s: %s%n", logFilePath, e.getMessage());
            logOutputAccumulator.append(errorMsg);
            logger.warn(errorMsg); 
        }
    }

    private Session createJschSession(ServerConfig serverConfig, StringBuilder log) throws JSchException {
        log.append(String.format("Creating JSch session for %s@%s:%d%n", serverConfig.getUsername(), serverConfig.getHost(), serverConfig.getPort()));
        JSch jsch = new JSch();
        Session session = jsch.getSession(serverConfig.getUsername(), serverConfig.getHost(), serverConfig.getPort());
        session.setPassword(serverConfig.getPassword());
        java.util.Properties config = new java.util.Properties();
        config.put("StrictHostKeyChecking", "no");
        config.put("PreferredAuthentications", "password"); // Explicitly prefer password auth
        session.setConfig(config);
        session.connect(30000); 
        log.append(String.format("JSch session connected.%n"));
        return session;
    }

    private String inferRemoteJarName(String mavenBuildOutput, String projectName, StringBuilder log) {
        logger.debug("Attempting to infer JAR name from Maven output. Length: {}", mavenBuildOutput.length());
        String buildingJarPattern = "Building jar: ";
        String jarSuffix = ".jar";
        // Corrected regex string from previous step
        String targetJarPatternString = ".*target" + "[/\\\\]" + "([^/\\\\\\\\\\\\\\\\\\\\\\\\]+)" + "\\\\\\\\.jar" + ".*";
        java.util.regex.Pattern targetJarPattern = java.util.regex.Pattern.compile(targetJarPatternString);

        for (String line : mavenBuildOutput.split("\r?\n")) {
            int indexBuildingJar = line.indexOf(buildingJarPattern);
            if (indexBuildingJar != -1) {
                String pathPart = line.substring(indexBuildingJar + buildingJarPattern.length());
                if (pathPart.endsWith(jarSuffix)) {
                    File jarFile = new File(pathPart); 
                    logger.info("Inferred JAR name from 'Building jar': {}", jarFile.getName());
                    return jarFile.getName();
                }
            }

            java.util.regex.Matcher matcher = targetJarPattern.matcher(line);
            if (matcher.matches()) { 
                String actualJarName = matcher.group(1).endsWith(jarSuffix) ? matcher.group(1) : matcher.group(1) + jarSuffix;
                File tempFile = new File(actualJarName);
                logger.info("Inferred JAR name from regex pattern: {}", tempFile.getName());
                return tempFile.getName();
            } else if (line.contains("[INFO] JARs built:") || line.contains("jar built successfully")) { 
                 String[] parts = line.split("[\\s\\:]+"); // Split by whitespace or colon
                 for (String part : parts) {
                     if (part.endsWith(jarSuffix) && (part.contains("/target/") || part.contains("\\\\target\\\\")) ) {
                          File jarFile = new File(part);
                          logger.info("Inferred JAR name from generic target path (split): {}", jarFile.getName());
                          return jarFile.getName();
                     }
                 }
            }
        }
        
        String fallbackJarName = projectName + "-0.0.1-SNAPSHOT.jar"; 
        logger.warn("Could not reliably infer JAR name from Maven output. Falling back to: {}. This may be incorrect.", fallbackJarName);
        log.append(String.format("[WARN] Could not reliably infer JAR name from Maven output. Falling back to: %s. This may be incorrect.%n", fallbackJarName));
        return fallbackJarName;
    }
} 